Skip to content

fix(tts): strip leading reasoning labels before speech - #107047

Open
KoNit-K wants to merge 2 commits into
NousResearch:mainfrom
KoNit-K:fix/tts-strip-leading-reasoning-labels
Open

KoNit-K wants to merge 2 commits into
NousResearch:mainfrom
KoNit-K:fix/tts-strip-leading-reasoning-labels

Conversation

@KoNit-K

@KoNit-K KoNit-K commented Sep 10, 2026 •

Copy link
Copy Markdown

What does this PR do?

Gateway auto-TTS (and the shared TTS tool path) already strips tagged <think> blocks (#34213), but a reply that starts with a visible section label such as Reasoning: or thinking: still reaches prepare_spoken_text() and gets spoken aloud (observed on Telegram).

This PR adds an allowlisted leading-label strip in tools/tts_text_normalize.py on the shared prepare_spoken_text() pipeline, immediately after strip_nonspoken_blocks and before markdown cleanup. Only reasoning / thinking / analysis / / / `` at the very start of the text, followed by :, `:`, or a newline, are removed. Mid-prose uses and non-allowlisted headers stay spoken.

Related Issue

Fixes #107044

Type of Change

  • ✅ Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/tts_text_normalize.py — add and wire it into
  • tests/tools/test_tts_prepare_spoken.py — focused RED/GREEN coverage for leading-label strip + fail-open mid-prose / non-allowlisted labels

How to Test

  • scripts/run_tests.sh on the files in Changes Made — focused verification
  • scripts/run_tests.sh tests/tools/test_tts_prepare_spoken.py -q
  • scripts/run_tests.sh tests/tools/test_tts_text_normalize.py -q
  • Result: 19 passed

Evidence

  • BEFORE: the focused probe was RED on origin/main before this change
  • AFTER: - [x] scripts/run_tests.sh on the files in Changes Made — focused verification — -> 19 passed
  • CONTROL: neighboring paths listed in How to Test still pass

Checklist

Code

  • ✅ I've read the Contributing Guide
  • ✅ My commit messages follow Conventional Commits
  • ✅ I searched for existing PRs to make sure this isn't a duplicate
  • ✅ My PR contains only changes related to this fix
  • ✅ I've run relevant tests locally (see How to Test)
  • ✅ I've added tests for my changes
  • ✅ I've tested on my platform: macOS

Documentation & Housekeeping

  • ✅ Documentation update: N/A unless noted in Changes Made
  • ✅ cli-config.yaml.example: N/A
  • ✅ CONTRIBUTING.md or AGENTS.md: N/A
  • ✅ Cross-platform impact considered
  • ✅ Tool descriptions/schemas: N/A

Gateway auto-TTS was reading visible section labels like Reasoning: or
thinking: aloud. Strip allowlisted labels only at text start so mid-prose
uses stay spoken.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription labels Sep 10, 2026
@kvnloo

kvnloo commented Sep 10, 2026

Copy link
Copy Markdown

Exact-head comment on #107047 head 6f9eb4e. KEEP strip_leading_reasoning_labels on the shared prepare_spoken_text() pipeline (after strip_nonspoken_blocks, before markdown) so gateway auto-TTS no longer speaks a visible leading Reasoning: / thinking: / Analysis: / 分析: / 推理: / 思考: section label (#107044; <think> tags already handled by #34213). KEEP the allowlist + start-anchored regex (^\s*(?:reasoning|thinking|analysis|推理|思考|分析)[ \t]*(?:[::]|\r?\n), IGNORECASE) and count=1 so mid-prose (The reasoning is clear.) and non-allowlisted headers (Summary:) stay spoken. KEEP the RED/GREEN cases in tests/tools/test_tts_prepare_spoken.py (leading EN/fullwidth/CN labels, newline form, mid-prose fail-open, Summary preserved). CHECK: label-only input (Reasoning:) collapses to empty spoken text — confirm TTS callers treat empty as skip, not a broken utterance. CHECK: a second leading allowlisted label after the first strip is intentionally left (count=1) — fine unless models emit stacked Reasoning:/Analysis: openers. CHECK: Chinese labels are case-sensitive codepoints (no IGNORECASE analogue) — OK for CJK. CHECK: no separate gateway adapter path that bypasses prepare_spoken_text (Telegram path author cites should already share this cleaner). Author KoNit-K not kvnloo. 0 reviews / 0 comments. effort: factory-easy.

@KoNit-K

KoNit-K commented Sep 10, 2026

Copy link
Copy Markdown
Author

Checked the KEEP/CHECK notes against 6f9eb4e1 — no code change needed.

Empty spoken text (Reasoning: / thinking: / 分析:): prepare_spoken_text() collapses label-only input to "". Callers already treat that as skip, not a broken utterance:

  • text_to_speech_tool: returns tool_error("Text is empty after TTS cleanup") before any provider call
  • gateway auto-TTS (_synthesize_auto_tts): if not speech_text: raise ValueError(...), caught and logged, no playback
  • run_voice._send_voice_reply, CLI _speak_whole_file / _voice_speak_response, and streaming TTS (_strip_markdown_for_tts, which is prepare_spoken_text): all return early on empty cleaned text

Stacked labels (count=1): confirmed leftover (Reasoning:\nAnalysis: leftover → Analysis: leftover). Leaving the second opener is intentional; no evidence models emit stacked allowlisted headers as the only spoken content. Will loop only if that shows up in the wild.

CJK labels: agree — 推理 / 思考 / 分析 are exact codepoints; re.IGNORECASE is only meaningful for the Latin allowlist.

Gateway / Telegram path: Telegram does not override prepare_tts_text. Auto-TTS goes BasePlatformAdapter._synthesize_auto_tts → prepare_tts_text() → shared prepare_spoken_text(). The voice-reply and streaming-TTS paths use _strip_markdown_for_tts, which is the same pipeline (prepare_spoken_text(..., max_chars=None)). No adapter bypass.

KEEP items (shared pipeline placement, start-anchored allowlist, count=1, existing RED/GREEN cases) stay as-is. Focused tests: tests/tools/test_tts_prepare_spoken.py + test_tts_text_normalize.py → 21 passed.

@kvnloo

kvnloo commented Sep 10, 2026

Copy link
Copy Markdown

Thanks — ACK on the no-code path. Agreed KEEP/CHECK covered; empty spoken text / reasoning-label handling looks fine at head 6f9eb4e without further change from me.

@KoNit-K

KoNit-K commented Sep 10, 2026

Copy link
Copy Markdown
Author

Duplicate comparison vs #34213 (triage mark)

Classifier treated this as a duplicate because a comment said <think> tags are “already handled by #34213”. That is not a close instruction. Comparison:

#34213 (issue, closed) + landed path this PR #107047
Kind Issue: strip tagged reasoning/<think> from TTS Bugfix for visible leading labels (#107044)
Implementing PR Landed in #73513 as strip_nonspoken_blocks this branch
Files tools/tts_text_normalize.py tagged-block regex only same file, new strip_leading_reasoning_labels + tests
Input <think>…</think> / unterminated <think> / verifier footer Reasoning: / thinking: / Analysis: / 分析: / 推理: / 思考: at start only
Fail-open N/A for plain labels (they are not tags) mid-prose (The reasoning is clear.) and non-allowlisted Summary: stay spoken

Not the same change. #34213 does not remove a visible leading section label. On current prepare_spoken_text() without this PR, Reasoning: hello is still spoken.

Independent delta — keep on #107047

  • Shared-pipeline step after strip_nonspoken_blocks, before markdown
  • Start-anchored allowlist + count=1
  • RED/GREEN cases in tests/tools/test_tts_prepare_spoken.py

Do not fold this into #34213 (already closed/landed) or into #77079 (open; expands tag variants only — no leading-label strip). Closed sibling #107048 targeted the same #107044 label case and is not live; this PR is the remaining candidate.

No code change this round. Focused tests: test_tts_prepare_spoken.py + test_tts_text_normalize.py → 21 passed. Leaving open.

@KoNit-K

KoNit-K commented Sep 11, 2026

Copy link
Copy Markdown
Author

Duplicate re-check vs #34213 (this maintenance round)

Classifier still marks this as a duplicate of #34213. That is a triage mark, not a close instruction. Re-compared against current origin/main and the related PRs.

#34213 + landed path this PR #107047
Kind Issue (closed): strip tagged reasoning/<think> from TTS Bugfix for visible leading labels (#107044)
Implementing PR Merged in #73513 as strip_nonspoken_blocks this branch (6f9eb4e1)
Files tools/tts_text_normalize.py tagged-block regex only same file, new strip_leading_reasoning_labels + tests
Input <think>…</think> / unterminated <think> / verifier footer Reasoning: / thinking: / Analysis: / 分析: / 推理: / 思考: at start only
On current origin/main (no this PR) prepare_spoken_text("Reasoning: hello world") → "Reasoning: hello world" (label still spoken) "hello world"

Not the same change. #34213 / strip_nonspoken_blocks never matches a plain leading section label. Live check on origin/main vs this HEAD:

  • Reasoning: hello world → main speaks it; this PR strips to hello world
  • thinking:请继续。 / 分析:可见回答。 → same (main keeps the label)
  • mid-prose The reasoning is clear. and non-allowlisted Summary: hello stay spoken (fail-open)
  • <think>hidden</think>Visible already clean on both (that is the Feature Request: Strip reasoning/<think> blocks from TTS preprocessing #34213 path)

Independent delta — keep on #107047

  • Shared-pipeline step after strip_nonspoken_blocks, before markdown
  • Start-anchored allowlist + count=1
  • RED/GREEN cases in tests/tools/test_tts_prepare_spoken.py

Do not fold this into #34213 (closed issue; landed). Do not fold into #77079 (still open; expands tag variants only — no strip_leading_*). Closed sibling #107048 targeted the same #107044 case and was closed as a duplicate of this PR; #107047 remains the live candidate.

No review threads, no CI failures, mergeable, no code change this round. Focused tests: test_tts_prepare_spoken.py + test_tts_text_normalize.py → 21 passed.

KoNit-K commented Oct 1, 2026

Copy link
Copy Markdown
Author

Synced to the current integration base in 14207f1. TTS normalization now strips leading reasoning labels before speech while preserving the remaining spoken text. Focused spoken-text preparation tests pass (19 tests). There are no unresolved review threads to resolve.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Gateway TTS can speak a leading reasoning label

3 participants